(二)Vue 的组件化开发

  在开发大型应用的时候,页面一般会划分为多个部分,但对不同的页面而言,可能有相同的部分(如头部导航,页脚)。
  若每个页面都独立开发,无疑会增加开发成本,因此一般会把页面的不同部分拆分为独立的组件,然后在不同的页面共享这些组件,达到复用效果。

  通常而言,一个应用会以一棵嵌套的组件树的形式来组织,如下图:

  例如,你可能会有页头、侧边栏、内容这些组件,它们可能又包含了其它如导航链接、博文之类的组件。

组件基础

组件的特点

  Vue 中组件的特点如下:

  • 组件可以进行任意次数的复用
  • 组件相当于 Vue 实例,能接收如datacomputedwatchmethods以及生命周期钩子等(除el外)属性。
  • 组件的data必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝,如:

    1
    2
    3
    4
    5
    data: function () {
    return {
    count: 0
    }
    }
  • 每个组件都会各自独立的维护它的count。因为你每用一次组件,就会有一个它的新实例被创建。

组件的基本使用步骤

  组件使用分为三个步骤:

  • ① 创建组件构造器
  • ② 注册组件
  • ③ 使用组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<!-- 3. 使用组件-->
<my-cpn></my-cpn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 1.创建组件构造器
const cpn = Vue.extend({
template: `<div><h1>组件的使用</h1></div>`
});

// 2. 注册组件:第一个参数是注册组件的标签名,第二个参数是前面定义的组件构造器
Vue.component('my-cpn', cpn);

const vue = new Vue({
el: "#app"
})
</script>

化繁为简——语法糖

  前面编写组件的方法太繁琐,官方推荐使用语法糖来省略部分代码,当然其本质还是会调用Vue.extend:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<!-- 2. 使用组件-->
<my-cpn></my-cpn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 1. 注册组件同时创建组件构造器:第一个参数是注册组件的标签名,第二个参数组件构造器
Vue.component('my-cpn', {
template: `<div><h1>组件的使用</h1></div>`
});

const vue = new Vue({
el: "#app"
})
</script>

组件的类型

  在模板中使用组件,其必须先注册才能被 Vue 识别 ,组件的注册类型又分为 2 种:

  • 全局组件
  • 局部组件

全局组件

  全局注册的组件可以在其被注册之后的任何(通过new Vue)新创建的 Vue 根实例中使用,也包括其组件树中的所有子组件的模板中,通过Vue.component即可注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<my-btn></my-btn>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 全局组件
Vue.component("my-btn", {
data() {
return {
count: 0,
}
},
template: `<button @click='count ++'>点击该按钮次数增加:{{count}}</button>`
});
const vue = new Vue({
el: "#app",
})
</script>

  全局组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是my-btn
  我们通过new Vue创建的 Vue 根实例中,可以把这个组件作为自定义元素来使用。

局部组件(常用)

  局部组件使用变量赋值的方式定义,之后通过在 Vue 实例中的components属性添加它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const component = {
data() {
return {
count: 0
}
},
template: '<button @click='count ++'>点击该按钮次数增加:{{count}}</button>'
};
const vue = new Vue({
el: '#app',
components: {
'my-component': component
}
})
</script>

  当然,语法糖使得其可以简写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
<script>
const vue = new Vue({
el: "#app",
components: {
"my-component": {
data() {
return {
count: 0,
}
},
template: "<button @click='count ++'>点击该按钮次数增加:{{count}}</button>"
};
}
})
</script>

注意哦:局部组件只能在相关 Vue 实例作用的标签中使用,且在其子组件中不可用。

  例如,若你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:

1
2
3
4
5
6
7
8
const ComponentA = { /* ... */ }

const ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}

  若通过 webpack 使用 ES 2015 模块,可以这么做:

1
2
3
4
5
6
7
8
import ComponentA from './ComponentA.vue'

export default {
components: {
ComponentA
},
// ...
}

  注意在 ES 2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写,即这个变量名同时是:

  • 用在模板中的自定义元素的名称
  • 包含了这个组件选项的变量名

模版分离

  由于在template模版里的代码看起来很乱,因此 Vue 提供了模版分离的 2 种方式:

  • 使用<script>标签(略)
  • 使用<template>标签添加id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<my-component></my-component>
</div>
<template id="temp">
<h1>抽离组件啦</h1>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vue = new Vue({
el: "#app",
components: {
"my-component": {
template: "#temp"
}
}
})
</script>

组件通信

  由于数据间经常交互,所以不同的组件之间应该可以相互通信,组件的通信又分为:

  • 父组件传递数据给子组件
  • 子组件传递数据(或事件)给父组件

什么是父子组件?

  组件有父子之分,子组件必须在父组件定义前定义,而父组件必须在 Vue 的实例中注册后才能使用。
  Vue 实例亦叫root组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<div id="app">
<!-- 4.最后使用父组件 -->
<fc></fc>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
<!-- 1.首先定义子组件 -->
const sCpn = {
template: `
<div>
<h1>我是子组件</h1>
</div>
`
};
<!-- 2.其次定义父组件并注册子组件 -->
const fCpn = {
template: `
<div>
<h1>我是父组件</h1>
<sc></sc>
</div>
`,
components: {
sc: sCpn
}
};

<!-- 3.再注册父组件 -->
const vue = new Vue({
el: "#app",
components: {
fc: fCpn
}
})
</script>

父子组件间的通信

  我们知道子组件是不能直接引用父组件或 Vue 实例中的数据的,但是在开发中经常有一些数据需要从上层传递到下层。
  比如在一个页面中,我们从服务器请求了大量数据。
  其中一部分数据,并非由整个页面的大(父)组件来显示,而是需要让下面的小(子)组件去显示。
  此时,并不会让子组件再次发送一个网络请求,而是直接让父组件将数据传递给子组件。

  那么,如何进行父子组件间的通信呢?

  Vue 官方对不同方向提供了不同的解决方案:

  • 父传子:可通过 prop向子组件传递数据
  • 子传父:可通过事件向父组件发送消息

父传子

  在子组件中,可以使用prop属性来接收父组件的数据,接收后可通过在组件标签中使用v-bind属性动态地将父组件的数据注入子组件中。

  官方说明:Prop 使你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。

Prop 传递值类型

  prop传递值的类型可以为:

  • 数字
  • 布尔值
  • 字符串
  • 数组:数组中的字符串(数字、布尔值)就是传递时的名称
  • 对象:对象可以设置传递时的类型,也可以设置默认值等

  下面仅介绍常用的 2 种:

数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 4.使用子组件并将父组件数据注入子组件中 -->
<div id="app">
<cpn :cproducts="products"></cpn>
</div>

<template id="tl">
<div>
<h1>{{cproducts}}</h1>
<ul>
<li v-for="c in cproducts">{{c}}</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
<!-- 1.定义子组件 -->
const cpn = {
<!-- 2.引用分离的模版 -->
template: '#tl',
data() {
return {};
},
props: ['cproducts']
};
<!-- 3.定义父组件(设置其数据)并注册子组件 -->
const vm = new Vue({
el: '#app',
data: {
products: ['苹果', '三星', '华为']
},
components: {
cpn
}
})
</script>

注意哦:这里的父组件为 Vue 实例哦,它也叫root组件呢!

  经过上面的理解后,可以将代码简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 4.使用子组件并将父组件数据注入子组件中 -->
<div id="app">
<cpn :products="products"></cpn>
</div>

<template id="tl">
<div>
<h1>{{products}}</h1>
<ul>
<li v-for="p in products">{{p}}</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
<!-- 1.定义子组件 -->
const cpn = {
<!-- 2.引用分离的模版 -->
template: '#tl',
data() {
return {};
},
props: ['products']
};
<!-- 3.定义父组件(设置数据)并注册子组件 -->
const vm = new Vue({
el: '#app',
data: {
products: ['苹果', '三星', '华为']
},
components: {
cpn
}
})
</script>

对象

  除了数组之外,亦可使用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- 4.使用子组件并将父组件数据注入子组件中 -->
<div id="app">
<cpn :person="person"></cpn>
</div>

<template id="tl">
<div>
<ul>
<li>{{person.name}}</li>
<li>{{person.age}}</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
<!-- 1.定义子组件 -->
const cpn = {
<!-- 2.引用分离的模版 -->
template: '#tl',
data() {
return {};
},
props: {
person: {}
}
};
<!-- 3.定义父组件(设置数据)并注册子组件 -->
const vm = new Vue({
el: '#app',
data: {
person: {
name: 'Lovike',
age: 18
}
},
components: {
cpn
}
})
</script>

类型检查

  一般在需要对props进行类型等验证时,就需要该写法了,验证支持以下数据类型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cpn = {
template: '#tl',
data() {
return {};
},
props: {
// 1. 类型限制
// products: String
// 2. 类型限制及提供默认值和必传值
products:{
type: String,
default: '默认值哦',
required: true
},
// 3. 对象或数组类型的默认值必须为从一个工厂函数获取
// cproducts:{
// type: Object,
// default() {
// return {}
// },
// required: true
// }
}
};

注意哦:子组件的props目前不支持驼峰命名,cproducts命名为cProducts可能有 Bug,官方推荐使用 kebab-case 的事件名:c-products

子传父

  除了父组件向子组件传递数据外,还有一种常见的是子组件传递数据或事件到父组件中,此时需要使用自定义事件来完成。
  自定义事件的流程如下:

  • 在子组件中,通过$emit()来发射事件;
  • 在父组件中,通过v-on来监听子组件事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- 父组件模版 -->
<div id="app">
<!-- 使用子组件的自定义事件,并传递给父组件的函数(自动获取参数) -->
<cpn @c-click="getSon"></cpn>
</div>
<!-- 子组件模版 -->
<template id="tl">
<div>
<button v-for="c in categories" @click="btnClick(c)">{{c.name}}</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const cpn = {
template: '#tl',
data() {
return {
categories: [
{id: 60001, name: "手机数码"},
{id: 60002, name: "电脑办公"},
{id: 60003, name: "时尚美妆"},
{id: 60004, name: "户外生活"},
]
};
},
methods: {
btnClick(c) {
// 传递自定义事件( c-click )给父组件
this.$emit('c-click', c);
}
}
};

const vm = new Vue({
el: '#app',
data: {
products: ['苹果', '三星', '华为']
},
components: {
cpn
},
methods: {
getSon(c) {
alert('id: ' + c.id + ' name: ' + c.name);
}
}
})
</script>

组件访问

  有时候我们需要直接访问组件里的一些数据,即让父组件直接访问子组件或子组件直接访问父组件。

父访子

  要想让父组件访问子组件的数据,可以:

  • 使用$children
  • 使用$refs(常用)

$children 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- Vue 实例模版 -->
<div id="app">
<cpn></cpn>
<button @click="getCpnData">查看子组件数据</button>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<p>我叫{{name}},今年{{age}}岁</p>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>

const vm = new Vue({
el: '#app',
methods: {
getCpnData() {
console.log(this.$children);
console.log(this.$children[0].name + ' ' + this.$children[0].age);
}
},
components: {
cpn: {
template: '#cpnt',
data() {
return {
name: 'lisi',
age: 18
}
}
}
}
})
</script>

$refs 方式

  使用该方式,需要在组件标签上添加ref属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- Vue 实例模版 -->
<div id="app">
<cpn></cpn>
<cpn ref="num2"></cpn>
<cpn></cpn>
<button @click="getCpnData">查看子组件数据</button>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<p>我叫{{name}},今年{{age}}岁</p>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>

const vm = new Vue({
el: '#app',
methods: {
getCpnData() {
console.log(this.$refs.num2);
console.log(this.$refs.num2.name + ' ' + this.$refs.num2.age);
}
},
...
})
</script>

子访父

  要想让子组件访问父组件的数据,可以使用$parent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- Vue 实例模版 -->
<div id="app">
<p>我叫{{name}},今年{{age}}岁</p>
<cpn></cpn>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<button @click="getCpnData">查看父组件数据</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'lisi',
age: 18
},
components: {
cpn: {
template: '#cpnt',
methods: {
getCpnData() {
console.log(this.$parent);
console.log(this.$parent.name + ' ' + this.$parent.age);
// 访问 root 组件
console.log(this.$root);
}
},
}
}
})
</script>

注意哦:可以通过$root访问Vue实例(根)组件的数据

插槽 slot

  组件的插槽能使封装的组件扩展性更强,让使用者决定组件内部的一些内容到底该显示什么。

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- Vue 实例模版 -->
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn>
<button>插槽</button>
</cpn>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<p>我是子组件</p>
<slot>默认值</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpnt',
}
}
})
</script>

具名插槽

  当子组件的功能复杂时,子组件的插槽可能并非时一个。
  比如封装一个导航栏的子组件,可能就需要 3 个插槽,分别代表左、中、右。
  那么在外面给插槽插入内容时,如何区分插入的是哪一个呢?
  此时需要通过给插槽命名来解决该问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- Vue 实例模版 -->
<div id="app">
<cpn><span slot="left">替换左插槽</span></cpn>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<slot name="left"></slot>
<slot name="center"></slot>
<slot name="right"></slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpnt',
}
}
})
</script>

作用域插槽

  作用域插槽可以在父组件替换插槽标签时,内容由子组件提供

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- Vue 实例模版 -->
<div id="app">
<cpn>
<template v-slot:default="fruits">
{{fruits}}
<ul>
<li v-for="f in fruits">{{f}}</li>
</ul>
</template>
</cpn>
</div>

<!-- 子组件模版 -->
<template id="cpnt">
<div>
<slot :fru="fruits">
<ul>
<li v-for="f in fruits">{{f}}</li>
</ul>
</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
const vm = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpnt',
data() {
return {
fruits: ['苹果', '香蕉', '橘子']
}
}
}
}
})
</script>

参考

0%